package com.czkevin.guicontrolsapi;

import com.czkevin.guicontrolsapi.controls.GUIControl;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.entity.HumanEntity;
import org.bukkit.entity.Player;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.inventory.InventoryEvent;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.InventoryHolder;
import org.bukkit.inventory.ItemStack;
import org.bukkit.scheduler.BukkitTask;

import java.util.*;
import java.util.function.Consumer;

public abstract class Window implements InventoryHolder, WithParentWindow {

    public static final ItemStack ITEM_SPACE = ItemUtils.putMeta(new ItemStack(Material.WHITE_STAINED_GLASS_PANE), false, "\u00a70 ", "\u00a70empty");
    public static final ItemStack ITEM_SCROLL_BAR = ItemUtils.putMeta(new ItemStack(Material.GRAY_STAINED_GLASS_PANE), false, "\u00a70 ", "\u00a70empty");

    public static final ItemStack ARROW_LEFT = HeadTools.HEAD_ARROW_LEFT;
    public static final ItemStack ARROW_RIGHT = HeadTools.HEAD_ARROW_RIGHT;
    public static final ItemStack ARROW_UP = HeadTools.HEAD_ARROW_UP;
    public static final ItemStack ARROW_DOWN = HeadTools.HEAD_ARROW_DOWN;

    private final String title;

    private final int width;
    private final int height;

    private static final int screenWidth = 8;
    private final int screenHeight;
    private boolean renderedScrollbar = false;

    private int offsetX = 0;
    private int offsetY = 0;

    private Map<HumanEntity, Window> parentMap = new HashMap<>();
    private Map<HumanEntity, Window> childMap = new HashMap<>();

    private Inventory gui;
    private GUIControl[] controls;

    private Set<HumanEntity> participants = new HashSet<>();
    private Set<HumanEntity> opened = new HashSet<>(); // to keep track opened windows, prevent calling onOpen() multiple times
    private Set<HumanEntity> allowed_to_close = new HashSet<>();

    private boolean update = false;
    private long update_period;
    private BukkitTask update_task = null;

    private boolean fillSpace = false;
    private boolean fillScrollBarSpace = false;

    public void setParent(HumanEntity target, Window parent, boolean override) {
        if(parentMap.containsKey(target) && !override) throw new IllegalArgumentException("Parent already set! ");
        this.parentMap.put(target, parent);
    }

    @Override
    public Window getParent(HumanEntity target) {
        return parentMap.get(target);
    }

    public Window(String title, int width, int height) {
        if(width < 8) width = 8;
        if(height < 1) height = 1;
        this.title = title;
        this.width = width;
        this.height = height;
        this.controls = new GUIControl[this.width * this.height];
        int size;
        if(height < 6) {
            screenHeight = height;
        } else {
            screenHeight = 5;
        }
        size = (screenHeight+1) * 9;
        this.gui = Bukkit.createInventory(this, size, title);
        render(true);
    }

    public String getTitle() {
        return title;
    }

    public int getOffsetX() {
        return offsetX;
    }

    public int getOffsetY() {
        return offsetY;
    }

    public int getScreenHeight() {
        return screenHeight;
    }

    public static int getScreenWidth() {
        return screenWidth;
    }

    public void setOffset(int newOffsetX, int newOffsetY) {
        if(newOffsetX > width-screenWidth) newOffsetX = width - screenWidth;
        if(newOffsetY > height-screenHeight) newOffsetY = height - screenHeight;
        if(newOffsetX < 0) newOffsetX = 0;
        if(newOffsetY < 0) newOffsetY = 0;
        offsetX = newOffsetX;
        offsetY = newOffsetY;
        render(true);
    }

    private int screenPosToIndex(int screenX, int screenY) {
        if(screenX > screenWidth) throw new IllegalArgumentException("Screen X must < 9! ");
        if(screenY >= screenHeight) throw new IllegalArgumentException("Screen Y overflow, must < " + screenHeight + "! ");
        return screenY * 9 + screenX;
    }

    private boolean isDisplayPositionOnScreen(int displayX, int displayY) {
        if(displayX >= offsetX && displayX < (offsetX + screenWidth)) {
            if(displayY >= offsetY && displayY < (offsetY + screenHeight)) {
                // System.out.println(String.format("(%d, %d) screen = YES", displayX, displayY));
                return true;
            }
        }
        // System.out.println(String.format("(%d, %d) screen = NO", displayX, displayY));
        return false;
    }

    private int displayPosToIndex(int x, int y) {
        if(x < 0) throw new IllegalArgumentException("X must > 0! " + String.format("(%d, %d)", x, y));
        if(y < 0) throw new IllegalArgumentException("Y must > 0! " + String.format("(%d, %d)", x, y));
        if(x >= width) throw new IllegalArgumentException("X overflow! " + String.format("(%d, %d)", x, y));
        if(y >= height) throw new IllegalArgumentException("Y overflow! " + String.format("(%d, %d)", x, y));
        return y*width+x;
    }
    public ItemStack getDisplayItemAt(int displayX, int displayY) {
        try {
            if (!isDisplayPositionOnScreen(displayX, displayY)) return null;
            int screenX = displayX - offsetX;
            int screenY = displayY - offsetY;
            int i = screenPosToIndex(screenX, screenY);
            return getInventory().getItem(i);
        } catch (IllegalArgumentException ex) {
            System.out.println("not on screen! ");
            return null;
        }
    }
    public void setDisplayItemAt(int displayX, int displayY, ItemStack item) {
        try {
            if (!isDisplayPositionOnScreen(displayX, displayY)) return;
            int screenX = displayX - offsetX;
            int screenY = displayY - offsetY;
            int i = screenPosToIndex(screenX, screenY);
            gui.setItem(i, item);
        } catch (IllegalArgumentException ex) {
            System.out.println("not on screen! ");
        }
    }

    public Window(String title, int width, int height, long update_period) {
        this(title, width, height);
        update = true;
        this.update_period = update_period;
    }

    @Override
    public Inventory getInventory() {
        return gui;
    }

    public int size() {
        return gui.getSize();
    }

    private final LinkedList<GUIControl> rendered = new LinkedList<>();

    public void preRender() { }
    public void postRender() { }
    public void preUpdate() { }
    public void postUpdate() { }

    private void render(boolean force) {
        preRender();
        for(int i = 0; i < gui.getSize(); i++) {
            int screenX = i % 9;
            int screenY = i / 9;
            if(screenX == 8) continue;
            if(screenY == screenHeight) continue;
            int displayX = offsetX + screenX;
            int displayY = offsetY + screenY;
            int controlIndex = displayPosToIndex(displayX, displayY);
            if(controls[controlIndex] != null) {
                if(rendered.contains(controls[controlIndex])) continue;
                controls[controlIndex].onRender(force);
                rendered.add(controls[controlIndex]);
            } else {
                if(fillSpace) {
                    gui.setItem(i,
                        ItemUtils.putMeta(ITEM_SPACE, String.format("(%d, %d)", screenX, screenY), String.format("\u00a77display: \u00a7e(%d, %d)", displayX, displayY))
                    );
                } else {
                    gui.setItem(i, null);
                }
            }
        }
        rendered.clear(); // CLEAR UP
        if(!renderedScrollbar) {
            renderedScrollbar = true;
            for(int ly = 1; ly <= screenHeight-2; ly++) {
                gui.setItem(ly*9+8, fillScrollBarSpace ? ITEM_SCROLL_BAR : null);
            }
            for(int lx = 1; lx <= 6; lx++) {
                gui.setItem(screenHeight*9+lx, fillScrollBarSpace ? ITEM_SCROLL_BAR : null);
            }
        }
        gui.setItem(
                screenHeight*9,
                ARROW_LEFT
        );
        gui.setItem(
                screenHeight*9 + 7,
                ARROW_RIGHT
        );
        gui.setItem(
                8,
                ARROW_UP
        );
        gui.setItem(
            (screenHeight-1)*9 + 8,
                ARROW_DOWN
        );
        gui.setItem(screenHeight*9 + 8, ItemUtils.putMeta(new ItemStack(Material.BOOK),
                "\u00a77current location",
                String.format("\u00a77(\u00a7e%d\u00a77, \u00a7e%d\u00a77)", offsetX, offsetY)));
        postRender();
    }

    public final void open(HumanEntity target) {
        open(target, null);
    }

    public final void open(HumanEntity target, Window parent) {
        if (parent != null) {
            setParent(target, parent, false);
            parent.childMap.put(target, this);
        }
        Bukkit.getScheduler().runTaskLater(GUIControlsAPI.getInstance(), () -> {
            if(target.getOpenInventory().getTopInventory().getHolder() != null && Window.class.isAssignableFrom(target.getOpenInventory().getTopInventory().getHolder().getClass())) {
                Window prev = ((Window) target.getOpenInventory().getTopInventory().getHolder());
                prev.parentMap.remove(target);
                prev.close(target);
                BukkitTools.nextTick(() -> target.openInventory(gui));
            } else {
                target.openInventory(gui);
            }

            participants.add(target);
            if (update && update_task == null) {
                update_task = Bukkit.getScheduler().runTaskTimer(GUIControlsAPI.getInstance(), new UpdateTask(), 1L, update_period);
                // System.out.println(String.format(">>>> [%s] TASK STARTED", title));
            }
        }, 1L);
    }

    public void forEachParcitipant(Consumer<HumanEntity> f) {
        participants.forEach(f);
    }

    public final void onOpen(HumanEntity target) {
        if(!opened.contains(target)) {
            opened.add(target);
            onOpenInternal(target);
        }
    }

    public abstract void onOpenInternal(HumanEntity target);

    public void onChildCallback(HumanEntity target, Window child) { }

    protected abstract boolean onCloseInternal(HumanEntity target);

    public void onDestroy() { }

    public void onClickedOutside(InventoryClickEvent e) {
        e.setCancelled(true);
    }

    public final boolean onClose(HumanEntity target) {
        if(!opened.contains(target)) return true;
        if(Player.class.isAssignableFrom(target.getClass())) {
            if(GUIControlsAPI.getInstance().getInputWindowManager().isOpened((Player) target)) return true;
        }
        if(childMap.containsKey(target)) return true;
        if(allowed_to_close.contains(target)) return true;
        boolean ok = onCloseInternal(target);
        if(ok) opened.remove(target);
        return ok;
    }

    public final void closeAll() {
        for(HumanEntity target : opened.toArray(new HumanEntity[0])) {
            close(target);
        }
    }

    public final boolean close(HumanEntity target) { return close(target, false); }
    public final boolean close(HumanEntity target, boolean ignoreParent) {
        if(!opened.contains(target)) return true;
        if(childMap.containsKey(target)) {
            Window child = childMap.get(target);
            child.allowed_to_close.add(target);
        }
        allowed_to_close.add(target);
        onCloseInternal(target);
        opened.remove(target);
        if(target.getOpenInventory().getTopInventory().getHolder() == this) {
            target.closeInventory();
            // System.out.println(String.format("[%s] HOLDER CONFIRMED WHEN CLOSE", title));
        }
        if(opened.size() == 0) {
            destroy();
            // System.out.println(String.format("[%s] destroy due to no viewers", title));
        }
        if(!ignoreParent) {
            Window parent = getParent(target);
            if(parent != null) {
                parent.open(target, null);
                parent.onChildCallback(target, this);
            }
        } else {
            parentMap.remove(target);
        }
        return true;
    }

    public final void destroy() {
        onDestroy();
        if(update_task != null) {
            update_task.cancel();
            update_task = null;
            System.out.println(String.format("<<<< [%s] TASK STOPPED", title));
        }
    }


    public void setControl(int displayX, int displayY, GUIControl control) {
        int index = displayPosToIndex(displayX, displayY);
        if(control != null && (displayX+control.getWidth() > width || displayY+control.getHeight() > height)) throw new IndexOutOfBoundsException("Not enough space for that control! ");
        if(controls[index] != null) {
            GUIControl old = controls[index];
            old.internal_RemoveFromWindow();
            for(int i = 0; i < controls.length; i++) {
                if(controls[i] == old) {
                    controls[i] = null;
                }
            }
        }
        if(control != null) {
            control.internal_DeployToWindow(this, displayX, displayY);
            for(int lx = 0; lx < control.getWidth(); lx++) {
                for(int ly = 0; ly < control.getHeight(); ly++) {
                    controls[displayPosToIndex(
                            displayX + lx,
                            displayY + ly
                    )] = control;
                }
            }
            control.onRender(true);
        }
    }

    private void detectDisconnected() {
        Iterator<HumanEntity> iterator = opened.iterator();
        while(iterator.hasNext()) {
            HumanEntity p = iterator.next();
            if(!p.isValid()) {
                iterator.remove();
                childMap.remove(p);
            }
        }
    }

    private class UpdateTask implements Runnable {
        @Override
        public void run() {
            // System.out.println(String.format("==== [%s] TASK UPDATE", title));
            preUpdate();
            detectDisconnected();
            render(false);
            postUpdate();
        }
    }

    public GUIControl getControlAt(int displayX, int displayY) {
        if(displayX >= width || displayY >= height) return null;
        return controls[displayPosToIndex(displayX, displayY)];
    }


    public static Window get(InventoryEvent e){
        return get(e.getInventory());
    }

    public static Window get(Inventory inv) {
        if(inv.getHolder() != null && Window.class.isAssignableFrom(inv.getHolder().getClass())) {
            return (Window) inv.getHolder();
        }
        return null;
    }

    public boolean isFillSpace() {
        return fillSpace;
    }

    public void setFillSpace(boolean fillSpace) {
        this.fillSpace = fillSpace;
    }

    public boolean isFillScrollBarSpace() {
        return fillScrollBarSpace;
    }

    public void setFillScrollBarSpace(boolean fillScrollBarSpace) {
        this.fillScrollBarSpace = fillScrollBarSpace;
    }

    public void removeAllControls() {
        Arrays.fill(controls, null);
    }
}
